/*
	This file is part of libswirl
        original code by flyinghead
*/
#include "license/bsd"



#include "modem.h"
#include "modem_regs.h"
#include "hw/holly/holly_intc.h"
#include "hw/sh4/sh4_sched.h"
#include "hw/sh4/sh4_mmio.h"
#include "hw/holly/sb.h"
#include "oslib/oslib.h"
#include "picoppp.h"

#define start_pppd start_pico
#define stop_pppd stop_pico
#define write_pppd write_pico
#define read_pppd read_pico


#define MODEM_COUNTRY_RES 0
#define MODEM_COUNTRY_JAP 1
#define MODEM_COUNTRY_USA 2

#define MODEM_MAKER_SEGA 0
#define MODEM_MAKER_ROCKWELL 1

#define MODEM_DEVICE_TYPE_336K 0

#ifdef RELEASE
#define LOG(...)
#else
#define LOG(...) do { printf("[%d.%03d] MODEM ", (int)os_GetSeconds(), (int)(os_GetSeconds() * 1000) % 1000); printf(__VA_ARGS__); putchar('\n'); } while (false);
#endif

struct ModemDevice_impl : MMIODevice {

	const  u32 MODEM_ID[2] =
	{
		MODEM_COUNTRY_RES,
		(MODEM_MAKER_ROCKWELL << 4) | (MODEM_DEVICE_TYPE_336K),
	};

	 modemreg_t modem_regs;

	u8 dspram[0x1000];

	int modem_sched;

	enum ModemStates
	{
		MS_INVALID,				//needs reset
		MS_RESET,				//reset is low
		MS_RESETING,			//reset is hi
		MS_ST_CONTROLER,		//Controller self test
		MS_ST_DSP,				//DSP self test
		MS_END_DSP,				//DSP self test end
		MS_NORMAL,	 			//Normal operation

	};
	ModemStates state = MS_INVALID;

	enum ConnectState
	{
		DISCONNECTED,
		DIALING,
		RINGING,
		HANDSHAKING,
		PRE_CONNECTED,
		CONNECTED,
	};
	ConnectState connect_state = DISCONNECTED;

	void update_interrupt()
	{
		modem_regs.reg1e.RDBIA = modem_regs.reg1e.RDBIE && modem_regs.reg1e.RDBF;
		modem_regs.reg1e.TDBIA = modem_regs.reg1e.TDBIE && modem_regs.reg1e.TDBE;

		if (modem_regs.reg1f.NCIA || modem_regs.reg1f.NSIA || modem_regs.reg1e.RDBIA || modem_regs.reg1e.TDBIA)
			asic->RaiseInterrupt(holly_EXP_8BIT);
		else
			asic->CancelInterrupt(holly_EXP_8BIT);
	}

	u32 get_masked_status(u32 reg)
	{
		u8 int_mask = dspram[regs_int_mask_addr[reg]];
		return int_mask & modem_regs.ptr[reg];
	}

#define SET_STATUS_BIT(reg, bit, value) do {		\
	if ((bit) != (value))							\
	{												\
		if (!modem_regs.reg1f.NSIE)					\
		{											\
			bit = (value);							\
		}											\
		else										\
		{											\
			u8 before = get_masked_status(reg);		\
			bit = (value);							\
			if (before != get_masked_status(reg))	\
				modem_regs.reg1f.NSIA = 1;			\
		}											\
	}												\
} while (false);

	double last_dial_time;

#ifndef RELEASE
	double last_comm_stats;
	int sent_bytes;
	int recvd_bytes;
	FILE* recv_fp;
	FILE* sent_fp;
#endif

	int modem_sched_func(int tag, int c, int j)
	{
#ifndef RELEASE
		if (os_GetSeconds() - last_comm_stats >= 2)
		{
			if (last_comm_stats != 0)
			{
				printf("Stats sent %d (%.2f kB/s) received %d (%.2f kB/s) TDBE %d RDBF %d\n", sent_bytes, sent_bytes / 2000.0,
					recvd_bytes, recvd_bytes / 2000.0,
					modem_regs.reg1e.TDBE, modem_regs.reg1e.RDBF);
				sent_bytes = 0;
				recvd_bytes = 0;
			}
			last_comm_stats = os_GetSeconds();
		}
#endif
		int callback_cycles = 0;

		switch (state)
		{
		case MS_ST_CONTROLER:
			ControllerTestEnd();
			break;
		case MS_ST_DSP:
			DSPTestStart();
			break;
		case MS_END_DSP:
			DSPTestEnd();
			break;
		case MS_NORMAL:
			modem_regs.reg1f.NEWC = 0;		// Not needed when state is CONNECTED but who cares

			switch (connect_state)
			{
			case DIALING:
				if (last_dial_time != 0 && os_GetSeconds() - last_dial_time > 0.99)
				{
					LOG("Switching to RINGING state");
					connect_state = RINGING;
					schedule_callback(100);
				}
				else
				{
					last_dial_time = os_GetSeconds();

					modem_regs.reg1e.TDBE = 1;
					schedule_callback(1000);	// To switch to Ringing state
				}
				break;
			case RINGING:
				last_dial_time = 0;
				LOG("\t\t *** RINGING STATE ***");
				modem_regs.reg1f.NEWS = 1;
				if (!modem_regs.reg09.DATA)
				{
					SET_STATUS_BIT(0x0f, modem_regs.reg0f.RI, 1);
					SET_STATUS_BIT(0x0b, modem_regs.reg0b.ATV25, 1);
				}
				break;
			case HANDSHAKING:
				LOG("\t\t *** HANDSHAKING STATE ***");
				if (modem_regs.reg12 == 0xAA)
				{
					// V8 AUTO mode
					dspram[0x302] |= 1 << 3;				// ANSam detected
				}
				modem_regs.reg1f.NEWS = 1;
				SET_STATUS_BIT(0x0f, modem_regs.reg0f.RI, 0);
				SET_STATUS_BIT(0x0b, modem_regs.reg0b.ATV25, 0);

				callback_cycles = SH4_MAIN_CLOCK / 1000000 * 500;		// 500 ms
				connect_state = PRE_CONNECTED;

				break;

			case PRE_CONNECTED:
				printf("MODEM Connected\n");
				if (modem_regs.reg03.RLSDE)
					SET_STATUS_BIT(0x0f, modem_regs.reg0f.RLSD, 1);
				if (modem_regs.reg12 == 0xAA)
				{
					// V8 AUTO mode
					dspram[0x302] |= 1 << 4;				// protocol octet received
					dspram[0x301] |= 1 << 4;				// JM detected
					dspram[0x303] |= 0xE0;					// Received protocol bits (?)
					dspram[0x2e3] |= 5;						// Symbol rate 3429
					dspram[0x239] |= 12;					// RTD 0 @ 3429 sym rate
					if (modem_regs.reg08.ASYN)
					{
						modem_regs.reg12 = 0xce;		// CONF V34 - K56flex
						modem_regs.reg0e.SPEED = 0x10;	// 33.6k
					}
					else
					{
						// Force the driver to ASYN=1 so it sends raw data
						modem_regs.reg12 = 0xa1;		// CONF V23 75 TX/1200 RX
						modem_regs.reg0e.SPEED = 0x02;	// 1.2k
					}
					if (modem_regs.reg1f.NSIE)
					{
						// CONF
						if (dspram[regs_int_mask_addr[0x12]] & (1 << 7))
							modem_regs.reg1f.NSIA = 1;
						// SPEED
						if (dspram[regs_int_mask_addr[0x0e]] & 0x1f)
							modem_regs.reg1f.NSIA = 1;
					}
					modem_regs.reg09.DATA = 1;
					modem_regs.reg15.AUTO = 0;
				}
				modem_regs.reg14 = 0x00;			// ABCODE: no error
				if (modem_regs.reg1f.NSIE)
				{
					// ABCODE
					if (dspram[regs_int_mask_addr[0x014]])
						modem_regs.reg1f.NSIA = 1;
				}
				modem_regs.reg1f.NEWS = 1;
				SET_STATUS_BIT(0x0f, modem_regs.reg0f.DSR, 1);
				if (modem_regs.reg02.v0.RTSDE)
					SET_STATUS_BIT(0x0f, modem_regs.reg0f.RTSDT, 1);

				// What is this? This is required for games to detect the connection
				SET_STATUS_BIT(0x0f, modem_regs.reg0f.FED, 1);

				// V.34 Remote Modem Data Rate Capability
				dspram[0x208] = 0xff;	// 2.4 - 19.2 kpbs supported
				dspram[0x209] = 0xbf;	// 21.6 - 33.6 kpbs supported, asymmetric supported

				start_pppd();
				connect_state = CONNECTED;
				callback_cycles = SH4_MAIN_CLOCK / 1000000 * 238;	// 238 us

				break;

			case CONNECTED:
#ifndef RELEASE
                for (int i = 0; i < sizeof(modem_regs); i++) {
                    LOG("modem_regs %02x == %02x", i, modem_regs.ptr[i]);
                }
#endif
				if (!modem_regs.reg1e.RDBF)
				{
					int c = read_pppd();
					if (c >= 0)
					{
						//LOG("pppd received %02x", c);
#ifndef RELEASE
						recvd_bytes++;
#endif
						modem_regs.reg00 = c & 0xFF;
						modem_regs.reg1e.RDBF = 1;
						if (modem_regs.reg04.FIFOEN)
							SET_STATUS_BIT(0x0c, modem_regs.reg0c.RXFNE, 1);
						SET_STATUS_BIT(0x01, modem_regs.reg01.RXHF, 1);
					}
				}
				modem_regs.reg1e.TDBE = 1;
				callback_cycles = SH4_MAIN_CLOCK / 1000000 * 238;	// 238 us

				break;

			default:
				break;
			}
			break;

		default:
			break;
		}
		update_interrupt();

		return callback_cycles;
	}

	bool Init()
	{
		modem_sched = sh4_sched_register(this, 0, STATIC_FORWARD(ModemDevice_impl, modem_sched_func));

		return true;
	}

	void schedule_callback(int ms)
	{
		sh4_sched_request(modem_sched, SH4_MAIN_CLOCK / 1000 * ms);
	}

#define SetReg16(rh,rl,v) {modem_regs.ptr[rh]=(v)>>8;modem_regs.ptr[rl]=(v)&0xFF; }

	void NormalDefaultRegs()
	{
		verify(state == MS_NORMAL);
		verify(sizeof(regs_write_mask) == sizeof(modem_regs));
		verify(sizeof(por_dspram) == sizeof(dspram));

		// Default values for normal state
		memset(&modem_regs, 0, sizeof(modem_regs));
		memcpy(dspram, por_dspram, sizeof(dspram));
		modem_regs.reg05.CEQ = 1;
		modem_regs.reg12 = 0x76;	// CONF: V.32 bis TCM 14400
		modem_regs.reg09.DATA = 1;
		modem_regs.reg09.DTMF = 1;
		modem_regs.reg15.HWRWK = 1;
		modem_regs.reg07.RDLE = 1;
		modem_regs.reg15.RDWK = 1;
		modem_regs.reg03.RLSDE = 1;
		modem_regs.reg02.TDE = 1;
		modem_regs.reg13.TLVL = 0x9;

		modem_regs.reg1e.TDBE = 1;
		connect_state = DISCONNECTED;
		last_dial_time = 0;
	}
	void DSPTestEnd()
	{
		verify(state == MS_END_DSP);
		state = MS_NORMAL;

		LOG("DSPTestEnd");
		NormalDefaultRegs();
	}
	void DSPTestStart()
	{
		verify(state == MS_ST_DSP);
		state = MS_END_DSP;
		LOG("DSPTestStart");

		modem_regs.reg1e.TDBE = 1;
		SetReg16(0x1B, 0x1A, 0xF083);	// EC checksum
		SetReg16(0x19, 0x18, 0x46EE); // Multiplier checksum
		SetReg16(0x17, 0x16, 0x00FA); // RAM checksum
		SetReg16(0x15, 0x14, 0x0A09); // ROM checksum
		SetReg16(0x13, 0x12, 0x3730); // Part number
		SetReg16(0x11, 0x00, 0x2041); // Revision level

		schedule_callback(50);
	}
	void ControllerTestEnd()
	{
		verify(state == MS_ST_CONTROLER);
		state = MS_ST_DSP;

		schedule_callback(50);
	}

	//End the reset and start internal tests
	void ControllerTestStart()
	{
		verify(state == MS_RESETING);
		//Set Self test values :)
		state = MS_ST_CONTROLER;
		//k, lets set values

		//1E:3 -> set
		modem_regs.reg1e.TDBE = 1;

		/*
			RAM1 Checksum  = 0xEA3C or 0x451
			RAM2 Checksum  = 0x5536 or 0x49A5
			ROM1 Checksum  = 0x5F4C
			ROM2 Checksum  = 0x3835 or 0x3836
			Timer/ROM/RAM  = 0x801 or 0xDE00
			Part Number    = 0x3730 or 0x3731
			Revision Level = 0x4241
		*/
		SetReg16(0x1D, 0x1C, 0xEA3C);
		SetReg16(0x1B, 0x1A, 0x5536);
		SetReg16(0x19, 0x18, 0x5F4C);
		SetReg16(0x17, 0x16, 0x3835);
		SetReg16(0x15, 0x14, 0x801);
		SetReg16(0x13, 0x12, 0x3730);
		SetReg16(0x11, 0x0, 0x4241);

		ControllerTestEnd();
	}

	void modem_reset(u32 v)
	{
		if (v == 0)
		{
			memset(&modem_regs, 0, sizeof(modem_regs));
			state = MS_RESET;
			LOG("Modem reset start ...");
		}
		else
		{
			stop_pppd();
			memset(&modem_regs, 0, sizeof(modem_regs));
			state = MS_RESETING;
			modem_regs.ptr[0x20] = 1;
			ControllerTestStart();
			printf("MODEM Reset\n");
		}
	}

	void check_start_handshake()
	{
		if (modem_regs.reg09.DTR
			&& (modem_regs.reg09.DATA || modem_regs.reg15.AUTO)
			&& connect_state == RINGING)
		{
			LOG("DTR asserted. starting handshaking");
			connect_state = HANDSHAKING;
			schedule_callback(1);
		}
	}

	bool word_dspram_write;
	bool module_download;
	u32 reg1b_save;
	u8 download_crc;

	void ModemNormalWrite(u32 reg, u32 data)
	{
#ifndef RELEASE
		if (recv_fp == NULL)
		{
			recv_fp = fopen("ppp_recv.dump", "w");
			sent_fp = fopen("ppp_sent.dump", "w");
		}
#endif
		//if (!module_download && reg != 0x10)
		//	LOG("ModemNormalWrite : %03X=%X", reg,data);
		u32 old = modem_regs.ptr[reg];
		modem_regs.ptr[reg] = (old & ~regs_write_mask[reg]) | (data & regs_write_mask[reg]);

		switch (reg)
		{
		case 0x02:
			modem_regs.reg0f.RTSDT = modem_regs.reg02.v0.RTSDE && connect_state == CONNECTED;
			break;

		case 0x06:
			LOG("PEN = %d", modem_regs.reg06.PEN);
			if (modem_regs.reg06.PEN)
				die("PEN = 1");
			if (modem_regs.reg06.HDLC)
				die("HDLC = 1");
			break;

		case 0x08:
			LOG("TPDM = %d ASYN = %d", modem_regs.reg08.TPDM, modem_regs.reg08.ASYN);
			break;

		case 0x09:
			check_start_handshake();	// DTR and DATA
			break;

		case 0x10:	// TBUFFER
			if (module_download)
			{
				download_crc = (download_crc << 1) + ((download_crc & 0x80) >> 7) + (data & 0xFF);
			}
			else if (connect_state == DISCONNECTED || connect_state == DIALING)
			{
				//LOG("ModemNormalWrite : TBUFFER = %X", data);
				if (connect_state == DISCONNECTED)
				{
					printf("MODEM Dialing\n");
					connect_state = DIALING;
				}
				schedule_callback(100);
			}
			else if (connect_state == CONNECTED && modem_regs.reg08.RTS)
			{
				//LOG("ModemNormalWrite : TBUFFER = %X", data);
#ifndef RELEASE
				sent_bytes++;
				if (sent_fp)
					fputc(data, sent_fp);
#endif
				write_pppd(data);
				modem_regs.reg1e.TDBE = 0;
			}
			break;

		case 0x11:
			LOG("PARSL = %d", modem_regs.reg11.PARSL);
			die("PARSL");
			break;

		case 0x14:	// ABCODE
			if (data == 0x4f || data == 0x5f)
			{
				reg1b_save = modem_regs.ptr[0x1b];
				modem_regs.ptr[0x1b] = data;
				//LOG("SPX/CTL download initiated");
				module_download = true;
				download_crc = 0;
			}
			else if (data == 0 && module_download)
			{
				// If module download is in progress, this signals the end of it
				modem_regs.reg17 = 0xFF;
				modem_regs.reg16 = download_crc;
				// Restore reg 1b
				modem_regs.ptr[0x1b] = reg1b_save;
				module_download = false;
				//LOG("SPX/CTL download finished CRC %02x", download_crc);
			}
			break;

		case 0x15:
			check_start_handshake();	// AUTO
			break;

			//Data Write Regs, for transfers to DSP
		case 0x18:	// MEDAL
			break;

		case 0x19:	// MEDAM
			word_dspram_write = true;
			break;

		case 0x1a:
			verify(connect_state != CONNECTED || !modem_regs.reg1a.SCIBE);
			break;

			//Address low
		case 0x1C:	// MEADDL
			break;

		case 0x1D:	// MEADDH
			if (modem_regs.reg1c_1d.MEMW && !(old & (1 << 5)))
			{
				word_dspram_write = false;
			}
			if (modem_regs.reg1c_1d.MEACC)
			{
				modem_regs.reg1c_1d.MEACC = 0;
				modem_regs.reg1f.NEWS = 1;
				u32 dspram_addr = modem_regs.reg1c_1d.MEMADD_l | (modem_regs.reg1c_1d.MEMADD_h << 8);
				if (modem_regs.reg1c_1d.MEMW)
				{
					//LOG("DSP mem Write%s address %08x = %x",
					//		word_dspram_write ? " (w)" : "",
					//		dspram_addr,
					//		modem_regs.reg18_19 );
					if (word_dspram_write)
					{
						if (dspram_addr & 1)
						{
							dspram[dspram_addr] = modem_regs.reg18_19 & 0xFF;
							dspram[dspram_addr + 1] = (modem_regs.reg18_19 >> 8) & 0xFF;
						}
						else
							*(u16*)&dspram[dspram_addr] = modem_regs.reg18_19;
					}
					else
						dspram[dspram_addr] = modem_regs.reg18_19 & 0xFF;
				}
				else
				{
					if (dspram_addr & 1)
						modem_regs.reg18_19 = dspram[dspram_addr] | (dspram[dspram_addr + 1] << 8);
					else
						modem_regs.reg18_19 = *(u16*)&dspram[dspram_addr];
					//LOG("DSP mem Read address %08x == %x",
					//		dspram_addr,
					//		modem_regs.reg18_19 );
				}
			}
			break;

		case 0x1F:
			if (!modem_regs.reg1f.NCIE)
				modem_regs.reg1f.NCIA = 0;
			if (modem_regs.reg1f.NEWC)
			{
				if (modem_regs.reg1a.SFRES)
				{
					modem_regs.reg1a.SFRES = 0;
					LOG("Soft Reset SET && NEWC, executing reset and init");
					modem_reset(1);
				}
				else
				{
					modem_regs.reg1f.NEWC = 0;	// accept the settings
					if (modem_regs.reg1f.NCIE)
						modem_regs.reg1f.NCIA = 1;
					LOG("NEWC CONF=%x", modem_regs.reg12);
				}
			}
			// Don't allow NEWS to be set if 0
			if ((old & (1 << 3)) == 0)
				modem_regs.reg1f.NEWS = 0;
			if (!modem_regs.reg1f.NEWS)
			{
				modem_regs.reg1f.NSIA = 0;
			}
			break;

		default:
			//printf("ModemNormalWrite : undef %03X=%X\n",reg,data);
			break;
		}
		update_interrupt();
	}

	u32 Read(u32 addr, u32 size)
	{
		u32 reg = addr & 0x7FF;
		verify((reg & 3) == 0);
		reg >>= 2;

		if (reg < 0x100)
		{
			verify(reg <= 1);
			return MODEM_ID[reg];
		}
		else
		{
			reg -= 0x100;
			if (reg < 0x21)
			{
				if (state == MS_NORMAL)
				{
					// Dial tone is detected if TONEA, TONEB and TONEC are set
					//if (reg==0xF)
					{
						SET_STATUS_BIT(0x0f, modem_regs.reg0f.CTS, modem_regs.reg08.RTS && connect_state == CONNECTED);
						SET_STATUS_BIT(0x0b, modem_regs.reg0b.TONEA, connect_state == DISCONNECTED);
						SET_STATUS_BIT(0x0b, modem_regs.reg0b.TONEB, connect_state == DISCONNECTED);
						SET_STATUS_BIT(0x0b, modem_regs.reg0b.TONEC, connect_state == DISCONNECTED);
						// FIXME This should be reset if transmit buffer is full
						if (modem_regs.reg04.FIFOEN || module_download)
							SET_STATUS_BIT(0x0d, modem_regs.reg0d.TXFNF, 1);
					}
					u8 data = modem_regs.ptr[reg];
					if (reg == 0x00)	// RBUFFER
					{
						//LOG("Read RBUFFER = %X", data);
						modem_regs.reg1e.RDBF = 0;
						SET_STATUS_BIT(0x0c, modem_regs.reg0c.RXFNE, 0);
						SET_STATUS_BIT(0x01, modem_regs.reg01.RXHF, 0);
#ifndef RELEASE
						if (connect_state == CONNECTED && recv_fp)
							fputc(data, recv_fp);
#endif
						update_interrupt();
					}
					else if (reg == 0x16 || reg == 0x17)
					{
						//LOG("SECTXB / SECTXB being read %02x", reg)
					}
					//else
					//	LOG("Read Reg %03x = %x", reg, data);

					return data;
				}
				else if (state == MS_ST_CONTROLER || state == MS_ST_DSP)
				{
					if (reg == 0x10)
					{
						modem_regs.reg1e.TDBE = 0;
						return 0;
					}
					else
					{
						return modem_regs.ptr[reg];
					}
				}
				else if (state == MS_RESETING)
				{
					return 0; //still reset
				}
				else
				{
					//LOG("Read (reset) reg %03x == %x", reg, modem_regs.ptr[reg]);
					return 0;
				}
			}
			else
			{
				LOG("modem reg %03X read -- wtf is it ?", reg);
				return 0;
			}
		}
	}

	void Write(u32 addr, u32 data, u32 size)
	{
		u32 reg = addr & 0x7FF;
		verify((reg & 3) == 0);
		reg >>= 2;



		if (reg < 0x100)
		{
			verify(reg <= 1);
			LOG("modem reg %03X write -- MODEM ID?!", reg);
		}
		else
		{
			reg -= 0x100;
			if (reg < 0x20)
			{
				if (state == MS_NORMAL)
				{
					ModemNormalWrite(reg, data);
				}
				else
				{
					LOG("modem reg %03X write %X -- undef state?", reg, data);
				}
				return;
			}
			else if (reg == 0x20)
			{
				//Hard reset
				modem_reset(data);
			}
			else
			{
				LOG("modem reg %03X write %X -- wtf is it?", reg, data);
				return;
			}
		}
	}

	ASIC* asic;

	ModemDevice_impl(ASIC* asic) : asic(asic) { }
};

MMIODevice* Create_Modem(ASIC* asic) {
	return new ModemDevice_impl(asic);
}
